package com.spzx.channel.service.impl;

import com.spzx.channel.service.ItemService;
import com.spzx.common.core.constant.SecurityConstants;
import com.spzx.common.core.domain.R;
import com.spzx.common.core.exception.ServiceException;
import com.spzx.common.redis.util.RedisLockUtil;
import com.spzx.product.api.RemoteProductService;
import com.spzx.product.domain.vo.ItemVO;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.util.Arrays;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class ItemServiceImpl implements ItemService {
    @Resource
    RemoteProductService remoteProductService;
    //装配的是 spzx-common-redis RedisConfig中初始化的对象，配置了键和值的序列化器
    // 使用redisTemplate存储的值，会通过FastJson2转为json字符串存入到redis中
    @Resource
    RedisTemplate redisTemplate;
    @Resource
    StringRedisTemplate stringRedisTemplate;

    /*
        openfeign拦截器：
            可以将feign远程访问的请求拦截 修改请求报文或者参数

        可以将业务流程总结：
            查询商品详情： 数据一致性问题(在product中更新商品信息时)
                1、bitmap解决缓存穿透问题：
                        前置操作：预加载skuId到redis的bitmap中
                2、查询缓存
                3、缓存命中失败，获取分布式锁(使用skuId拼接锁的key)  缓存穿透雪崩击穿
                    - 设置唯一uuid避免误删除
                    - 设置过期时间避免死锁
                    - 看门狗线程自动续期避免锁失效
                4、再次查询缓存
                5、远程调用查询数据库
                6、存到缓存中：
                    - 空数据设置较短过期时间
                7、释放分布式锁
     */
    //根据skuId查询商品详情业务
    @Override
    public ItemVO selectItemBySkuId(Long skuId) {
        //判断查询的skuId是不是数据库存在的sku的id值，如果是 可以继续执行，如果不是，不需要执行
        //= 1、商品服务启动时，需要加载所有的skuId存到redis的bitmap中
        //= 2、查询业务中 通过bitmap判断skuId是否存在
        Boolean exists = stringRedisTemplate.opsForValue().getBit("spzx:product:skuIds", skuId);
        if(!exists){
            //查询的skuId sku表中不存在
            throw new ServiceException(skuId+" 的商品数据不存在!");
        }
        //1、查询是否存在缓存  如果存在直接返回
        String key = "spzx:channel:item:" + skuId;
        Object cache = redisTemplate.opsForValue().get(key);
        if (!ObjectUtils.isEmpty(cache)) {
            return (ItemVO) cache;
        }
        //2、查询缓存失败：远程调用查询数据库
        //！！！优化： 查询数据库之前 ，需要获取分布式锁 ，查询skuId=2 和skuId=3的请求 需要获取不同的分布式锁
        //     使用锁的目的是为了保证 一批操作相同数据的请求 只有一个查询数据库 其它的阻塞等待查询缓存
        String lockKey = "spzx:channel:lock:"+skuId;
        String uuid = UUID.randomUUID().toString().replace("-","");
        RedisLockUtil.lock(stringRedisTemplate,lockKey ,uuid , 10);
        //线程获取到锁以后 再次查询缓存，避免自己获取锁失败自旋时，前面的线程已经查询到数据并设置到缓存中了
        try {
            cache = redisTemplate.opsForValue().get(key);
            if (!ObjectUtils.isEmpty(cache)) {
                return (ItemVO) cache;
            }

            //商品数据都存在product库中 需要 spzx-product服务查询
            //只能等 product服务暴露一个接口 远程调用查询得到商品详情
            R<ItemVO> itemVOR = remoteProductService.getItemVOBySkuId(skuId);
            if (itemVOR.getCode() != 200) {
                //远程调用失败
                throw new ServiceException(itemVOR.getMsg());
            }

            //3、缓存数据
            //redis最好设置过期时间：如果没有设置，并且redis配置了默认的rdb持久化，以后这些数据一直存在redis中
            //空数据可以设置较短的过期时间 缓存数据设置较长的过期时间
            redisTemplate.opsForValue().set(key, itemVOR.getData() ,
                    itemVOR.getData()==null?60:3600
                    , TimeUnit.SECONDS);

            //4、释放分布式锁

            //返回查询到的结果
            return itemVOR.getData();
        } finally {
            //在finally中释放锁：避免业务出现运行时异常 导致锁未释放
            RedisLockUtil.unlock(stringRedisTemplate,lockKey,uuid);
        }
    }


    @Override
    public void testLock() {
        //== 1、获取分布式锁 setnx  设置10秒的过期时间
        String uuid = UUID.randomUUID().toString().replace("-", "");
        String lockKey = "spzx:channel:test:lock";
        int timeout = 10;
        //使用工具类获取分布式锁
        RedisLockUtil.lock(stringRedisTemplate, lockKey, uuid, timeout);
        try {
            //== 2、获取分布式锁成功 执行number+1业务
            //1、获取redis中的number
            Object obj = redisTemplate.opsForValue().get("number");
            if (obj == null) {
                //初始化redis中的数据
                redisTemplate.opsForValue().set("number", 0);
                obj = 0;
            }
            //2、执行number+1
            Integer number = Integer.parseInt(obj.toString());
//                int i = 1/0;
//            TimeUnit.SECONDS.sleep(1000);
            //3、更新到redis中
            redisTemplate.opsForValue().set("number", number + 1);
            //调用工具类释放锁
            RedisLockUtil.unlock(stringRedisTemplate,lockKey,uuid);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }


    //分布式锁V6版：基于v5 解决v5 锁失效问题(业务时长超过锁过期时间)
    //在获取锁成功后开一个子线程对锁自动续期
    public void testLockV6() {
        //== 1、获取分布式锁 setnx  设置10秒的过期时间
        String uuid = UUID.randomUUID().toString().replace("-", "");
        String lockKey = "spzx:channel:test:lock";
        int timeout = 10;
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,
                uuid, timeout, TimeUnit.SECONDS);
        try {
            //== 2、获取分布式锁成功 执行number+1业务
            if (flag) {
                //开启看门狗线程: 锁的键、锁的值、锁的过期时间
                renewLock(lockKey, uuid, timeout);
                //获取分布式锁成功
                //1、获取redis中的number
                Object obj = redisTemplate.opsForValue().get("number");
                if (obj == null) {
                    //初始化redis中的数据
                    redisTemplate.opsForValue().set("number", 0);
                    obj = 0;
                }
                //2、执行number+1
                Integer number = Integer.parseInt(obj.toString());
//                int i = 1/0;
                TimeUnit.SECONDS.sleep(1000);
                //3、更新到redis中
                redisTemplate.opsForValue().set("number", number + 1);
                //== 自己获取到锁执行完业务以后  释放自己获取到的分布式锁 del
                //先判断锁是不是当前线程的
//                if(uuid.equals(redisTemplate.opsForValue().get("spzx:channel:test:lock"))){
//                    //是自己的锁 可以释放锁
//                    redisTemplate.delete("spzx:channel:test:lock");
//                }
                String delLockLua = "if KEYS[2] == redis.call('get' , KEYS[1]) " +
                        " then return redis.call('del' , KEYS[1]) " +
                        " else return 0 end";
                //execute执行lua脚本的方法
                Boolean delFlag = (Boolean) stringRedisTemplate.execute(new DefaultRedisScript(delLockLua, Boolean.class),
                        Arrays.asList(lockKey, uuid));
                //"89b25e36a3014778b18664405f073fd1"  spzx:channel:test:lock
                System.out.println(delFlag ? "释放锁成功" : "释放锁失败");
//8277823f8db94c5eaa027a0a17e3af75
            }
            //3、获取分布式锁失败
            else {
                //获取分布式锁失败：再次尝试获取分布式锁
                //自旋获取锁： 可能会导致SOF： stackoverflow
                //休眠一会 再次获取锁
                Thread.sleep(50 + new Random().nextInt(20, 100));
                this.testLockV6();

            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    private void renewLock(String lockKey, String uuid, int timeout) {
        // 等锁过期时间的2/3之后给锁续期
        new Thread(() -> {
            try {
                //1、让看门狗线程休眠2/3秒后 进行续期
                TimeUnit.SECONDS.sleep(timeout * 2 / 3);
                //2、判断锁是不是自己的，如果是则续期: 如果while循环不执行表示锁不是自己的看门狗线程结束
                while (uuid.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                    stringRedisTemplate.expire(lockKey, timeout, TimeUnit.SECONDS);
                    //3、续期后 再次休眠2/3的过期时间 进行下一次续期
                    TimeUnit.SECONDS.sleep(timeout * 2 / 3);
                }

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();


    }


    //分布式锁V5版：基于v4 解决v4删除锁原子性问题  使用LUA脚本解决
    public void testLockv5() {
        //== 1、获取分布式锁 setnx  设置10秒的过期时间
        String uuid = UUID.randomUUID().toString().replace("-", "");
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("spzx:channel:test:lock",
                uuid, 1000, TimeUnit.SECONDS);
        try {
            //== 2、获取分布式锁成功 执行number+1业务
            if (flag) {
                //获取分布式锁成功
                //1、获取redis中的number
                Object obj = redisTemplate.opsForValue().get("number");
                if (obj == null) {
                    //初始化redis中的数据
                    redisTemplate.opsForValue().set("number", 0);
                    obj = 0;
                }
                //2、执行number+1
                Integer number = Integer.parseInt(obj.toString());
//                int i = 1/0;
                //3、更新到redis中
                redisTemplate.opsForValue().set("number", number + 1);
                //== 自己获取到锁执行完业务以后  释放自己获取到的分布式锁 del
                //先判断锁是不是当前线程的
//                if(uuid.equals(redisTemplate.opsForValue().get("spzx:channel:test:lock"))){
//                    //是自己的锁 可以释放锁
//                    redisTemplate.delete("spzx:channel:test:lock");
//                }
                String delLockLua = "if KEYS[2] == redis.call('get' , KEYS[1]) " +
                        " then return redis.call('del' , KEYS[1]) " +
                        " else return 0 end";
                //execute执行lua脚本的方法
                Boolean delFlag = (Boolean) stringRedisTemplate.execute(new DefaultRedisScript(delLockLua, Boolean.class),
                        Arrays.asList("spzx:channel:test:lock", uuid));
                //"89b25e36a3014778b18664405f073fd1"  spzx:channel:test:lock
                System.out.println(delFlag ? "释放锁成功" : "释放锁失败");
//8277823f8db94c5eaa027a0a17e3af75
            }
            //3、获取分布式锁失败
            else {
                //获取分布式锁失败：再次尝试获取分布式锁
                //自旋获取锁： 可能会导致SOF： stackoverflow
                //休眠一会 再次获取锁
                Thread.sleep(50 + new Random().nextInt(20, 100));
                this.testLockv5();

            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    //分布式锁V4版：基于v3 解决v3 释放锁、锁续期无法判断锁是不是当前线程的问题  给锁设置线程唯一标志 避免误删除！！
    public void testLockV4() {
        //== 1、获取分布式锁 setnx  设置10秒的过期时间
        String uuid = UUID.randomUUID().toString().replace("-", "");
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("spzx:channel:test:lock",
                uuid, 10, TimeUnit.SECONDS);
        try {
            //== 2、获取分布式锁成功 执行number+1业务
            if (flag) {
                //获取分布式锁成功
                //1、获取redis中的number
                Object obj = redisTemplate.opsForValue().get("number");
                if (obj == null) {
                    //初始化redis中的数据
                    redisTemplate.opsForValue().set("number", 0);
                    obj = 0;
                }
                //2、执行number+1
                Integer number = Integer.parseInt(obj.toString());
//                int i = 1/0;
                //3、更新到redis中
                redisTemplate.opsForValue().set("number", number + 1);
                //== 自己获取到锁执行完业务以后  释放自己获取到的分布式锁 del
                //先判断锁是不是当前线程的
                if (uuid.equals(redisTemplate.opsForValue().get("spzx:channel:test:lock"))) {
                    //是自己的锁 可以释放锁
                    redisTemplate.delete("spzx:channel:test:lock");
                }
            }
            //3、获取分布式锁失败
            else {
                //获取分布式锁失败：再次尝试获取分布式锁
                //自旋获取锁： 可能会导致SOF： stackoverflow
                //休眠一会 再次获取锁
                Thread.sleep(50 + new Random().nextInt(20, 100));
                this.testLockV4();

            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }


    //分布式锁V3版：基于v2实现，解决v2的死锁问题
    public void testLockV3() {
        //== 1、获取分布式锁 setnx  设置10秒的过期时间
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("spzx:channel:test:lock",
                "1", 10, TimeUnit.SECONDS);
        try {
            //== 2、获取分布式锁成功 执行number+1业务
            if (flag) {
                //获取分布式锁成功
                //1、获取redis中的number
                Object obj = redisTemplate.opsForValue().get("number");
                if (obj == null) {
                    //初始化redis中的数据
                    redisTemplate.opsForValue().set("number", 0);
                    obj = 0;
                }
                //2、执行number+1
                Integer number = Integer.parseInt(obj.toString());
//                int i = 1/0;
                //3、更新到redis中
                redisTemplate.opsForValue().set("number", number + 1);
                //== 自己获取到锁执行完业务以后  释放自己获取到的分布式锁 del
                redisTemplate.delete("spzx:channel:test:lock");
            }
            //3、获取分布式锁失败
            else {
                //获取分布式锁失败：再次尝试获取分布式锁
                //自旋获取锁： 可能会导致SOF： stackoverflow
                //休眠一会 再次获取锁
                Thread.sleep(50 + new Random().nextInt(20, 100));
                this.testLockV3();

            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    //锁的测试业务
    //分布式锁：V2
    public void testLockV2() {
        //== 1、获取分布式锁 setnx
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("spzx:channel:test:lock", "1");
        try {
            //== 2、获取分布式锁成功 执行number+1业务
            if (flag) {
                //获取分布式锁成功
                //1、获取redis中的number
                Object obj = redisTemplate.opsForValue().get("number");
                if (obj == null) {
                    //初始化redis中的数据
                    redisTemplate.opsForValue().set("number", 0);
                    obj = 0;
                }
                //2、执行number+1
                Integer number = Integer.parseInt(obj.toString());
                int i = 1 / 0;
                //3、更新到redis中
                redisTemplate.opsForValue().set("number", number + 1);
                //== 自己获取到锁执行完业务以后  释放自己获取到的分布式锁 del
                redisTemplate.delete("spzx:channel:test:lock");
            }
            //3、获取分布式锁失败
            else {
                //获取分布式锁失败：再次尝试获取分布式锁
                //自旋获取锁： 可能会导致SOF： stackoverflow
                //休眠一会 再次获取锁
                Thread.sleep(50 + new Random().nextInt(20, 100));
                this.testLockV2();

            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }


    //V1:本地锁测试
    public synchronized void testLockV1() {
        //1、获取redis中的number
        Object obj = redisTemplate.opsForValue().get("number");
        if (obj == null) {
            //初始化redis中的数据
            redisTemplate.opsForValue().set("number", 0);
            obj = 0;
        }
        //2、执行number+1
        Integer number = Integer.parseInt(obj.toString());

        //3、更新到redis中
        redisTemplate.opsForValue().set("number", number + 1);
    }
}
